package maneger;

import com.sun.jmx.snmp.tasks.ThreadService;
import dao.FileDao;
import dao.FileMeta;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *  这个类就是 文件管理器 搜索文件的,针对目录进行扫描文件,并把扫描的结果同步更新到数据库中去
 */
public class FileManager {
    /**
     *  初始化选手数量为 1，当线程池执行完所有任务之后，就立即调用一次 countDown 撞线
     *  相当于就只有一次撞线的机会，也就是 调用一次 countDown 此时就会解除阻塞，后续再调用 await 就不会再堵塞了
     *  所以改成 null，每次调用 scanAll 都 new 新的 CountDownLatch
     */
    // private CountDownLatch countDownLatch = new CountDownLatch(1);
    private CountDownLatch countDownLatch = null;
    private FileDao fileDao = new FileDao();

    // 参数代表开始为 0 ， 是一个衡量任务结束的计数器
    // 这个就是原子性操作的自增自减，所以就线程安全的
    // 用一个 int 类型的计数器呢，多线程下会不安全
    private AtomicInteger taskCount = new AtomicInteger(0);

    // 通过这个方法, 实现针对 basePath 描述的目录, 内容的扫描.
    // 把这里的文件和子目录都扫描清楚, 并且保存到数据库中.
    public void scanAll(File basePath) {
        System.out.println("[FileManager] scanAll 扫描开始!" );
        // 记录一下时间
        long begin = System.currentTimeMillis();

        // 保证每次调用 scanAll 都重新创建了 CountDownLatch，重新进行计数了
        countDownLatch = new CountDownLatch(1);
        // scanAllFileByOneThread(basePath);
        // 换成多线程的
        scanAllByThreadPool(basePath);
        /**
         *  换成多线程有个小问题，由于只是把扫描工作交给线程池完成，主线程负责遍历目录
         *  存在一种可能： 主线程瞬间遍历完了，扫描工作还没完成，此时计时就会停止， 递归是很快的
         *  但此时就会往下执行，计算时间，时间就会不准确，计算的时间就会差的很多，
         *  所以需要等扫描工作完了之后，再计时停止
         */
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("scanAll 扫描结束! 执行时间: " + (end - begin) + " ms");
    }

    /**
     *  单线程遍历目录下的所有文件
     * @param basePath
     */
    private void scanAllFileByOneThread(File basePath) {
        if(!basePath.isDirectory()) {
            // 如果不是目录,不用递归扫描
            return;
        }
        // 先更新数据库
        scan(basePath);
        // 先列出目录下的所有文件
        File[] files = basePath.listFiles();
        // 是文件夹,遍历文件夹
        if(null == files || files.length == 0) {
            // 说明目录下没东西
            return;
        }
        // 递归遍历每个目录下的文件
        for (File file : files) {
            // 还要先判断根目录下的文件是不是也是目录,只有也是目录的时候才递归
            if(file.isDirectory()) {
                scanAllFileByOneThread(file);
            }
        }
    }

    /**
     * 上面那个方法是单线程的，此处我们提供一个多线程的方法，提高扫描速度
     * 用到线程池
     */
    // 用到线程池，得先有一个线程池, 给8个线程数
    // 加上 static ， 这个线程池就变成一个单例的了
    // 保证无论 new 多少个 FileManager，都保证线程池只有一份
    private static ExecutorService executorService = Executors.newFixedThreadPool(8);

    private void scanAllByThreadPool(File basePath) {
        if (!basePath.isDirectory()) {
            return;
        }
        // 一进入扫描，计数器自增, 也就是每次有一个任务就自增，用于判断扫描结果是否结束
        taskCount.getAndIncrement(); // taskCount++;

        // 是目录，扫描工作放到线程池里进行,扫描比较耗时，放到线程池中
        // 提交一个任务，匿名内部类的写法。注册到一个任务到线程池中去
        // 扫描操作，放到线程池里完成
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    scan(basePath);
                } catch (Exception e) {

                } finally {
                    /**
                     *  此处还有一个问题 ：上面的 scan(basePath); 出错了，就会执行不到下面的自减操作，那么任务永远不为 0
                     *  唤醒不了 scanAll（）方法中的主线程，计时不会停止了
                     *  所以解决办法： finally
                     *  catch 写不写都可以的
                     */
                    // 自减操作放到 finally 中，确保可以执行到
                    // 扫描任务完成 ， 计数器减减
                    taskCount.getAndDecrement(); // taskCount--;
                    if (taskCount.get() == 0) {
                        // 任务数为 0 ，通知唤醒 scanAll（）方法中的主线程，扫描结束，可以停止计时了
                        // countDown() 方法就是用来唤醒 countDownLatch.await(); 的，每一次任务执行完毕，手动调用 latch.await() 方法。 在 CountDownLatch 内部的计数器同时自减
                        countDownLatch.countDown();
                    }
                }
            }
        });
        // 递归还是放到主线程中进行
        // 先列出目录下的文件
        File[] files = basePath.listFiles();
        if(files == null || files.length == 0) {
            // 目录下没有文件或者目录
            return;
        }
        for (File file : files) {
            // 遍历目录下的每一个文件，判断是不是目录，是目录，递归，不是，不做操作
            if (file.isDirectory()) {
                scanAllByThreadPool(file);
            }
        }
    }

    /**
     *  方法作用 : 判断扫描到的文件和数据库是否相同,用于更新数据库
     * @param path
     */
    // scan 方法针对一个目录进行处理. (整个遍历目录过程中的基本操作)
    // 这个方法只针对当前 path 对应的目录进行分析.
    // 列出这个 path 下包含的文件和子目录, 并且把这些内容更新到数据库中.
    // 此方法不考虑子目录里面的内容.
    private void scan(File path) {
        // DEBUG 这个日志打印的有点多, 先临时去掉.
        System.out.println("[FileManager] 扫描路径: " + path.getAbsolutePath());

        // 1.列出文件系统上真实的文件/目录
        List<FileMeta> scanned = new ArrayList<>();
        // path.listFiles(); 这个 api 就是可以列出真实的目录
        File[] files = path.listFiles();
        if(files != null) {
            for (File file : files) {
                scanned.add(new FileMeta(file));
            }
        }

        // 2.列出数据库中当前指定目录里的内容
        List<FileMeta> saved = fileDao.searchByPath(path.getPath());
        // 3.找出文件系统中没有的,数据库中有的,把这些内容从数据库中删除掉
        List<FileMeta> forDelete = new ArrayList<>();
        for (FileMeta fileMeta : saved) {
            /**
             *  注意: 此处还有一个注意点,这里的 contains 本质上还是在比较 "对象是否相等",
             *  内部会调用 equals 方法,判断两个对象是否相等, equals 方法默认比较的是两个对象的地址,
             *  如果需要根据对象的内容进行比较，就需要重写 equals() 方法。
             *  在 实体类 FileMeta 中重写 equals 方法
             */
            if(!scanned.contains(fileMeta)) {
                // 文件系统中没有,而数据库中有,说明文件已经被删掉了,那么数据库中也要进行更新,删掉相应的部分
                // 这个操作就是把数据库中有的, 文件系统中没有的, 拎出来了.
                // 根据名字，路径 和 是否是文件夹俩判断是不是同一个
                forDelete.add(fileMeta);
            }
        }
        fileDao.delete(forDelete);

        // 4.找出文件系统中有的,数据库中没有的,把这些文件内容添加到数据库中去
        List<FileMeta> forAdd = new ArrayList<>();
        for (FileMeta fileMeta : scanned) {
            if(!saved.contains(fileMeta)) {
                // 找出了文件系统中有, 数据库没有的
                forAdd.add(fileMeta);
            }
        }
        fileDao.add(forAdd);
    }
}


/**
 *
 */
//public class FileManager {
//    private FileDao fileDao = new FileDao();
//    // 初始化设置选手数目为 1, 当线程池执行完所有任务之后, 就立即调用一次 countDown 撞线.
//
//
//    // 通过这个方法, 实现针对 basePath 描述的目录, 内容的扫描.
//    // 把这里的文件和子目录都扫描清楚, 并且保存到数据库中.
//    public void scanAll(File basePath) {
//        System.out.println("[FileManager] scanAll 开始!");
//        long beg = System.currentTimeMillis();
//
//
//        // scanAllByOneThread(basePath);
//        scanAllByThreadPool(basePath);
//
//
//        long end = System.currentTimeMillis();
//        System.out.println("[FileManager] scanAll 结束! 执行时间: " + (end - beg) + " ms");
//    }
//
//    private void scanAllByOneThread(File basePath) {
//        if (!basePath.isDirectory()) {
//            return;
//        }
//
//        // 先针对当前目录进行扫描
//        scan(basePath);
//
//        // 列出当前目录下包含的所有文件
//        File[] files = basePath.listFiles();
//        if (files == null || files.length == 0) {
//            // 当前目录下没东西.
//            return;
//        }
//        for (File f : files) {
//            if (f.isDirectory()) {
//                scanAllByOneThread(f);
//            }
//        }
//    }
//
//
//
//    private void scanAllByThreadPool(File basePath) {
//        if (!basePath.isDirectory()) {
//            return;
//        }
//
//        // 更新数据库
//        scan(basePath);
//
//        // 继续递归其他目录.
//        File[] files = basePath.listFiles();
//        if (files == null || files.length == 0) {
//            return;
//        }
//        for (File f : files) {
//            if (f.isDirectory()) {
//                scanAllByThreadPool(f);
//            }
//        }
//    }
//
//    // scan 方法针对一个目录进行处理. (整个遍历目录过程中的基本操作)
//    // 这个方法只针对当前 path 对应的目录进行分析.
//    // 列出这个 path 下包含的文件和子目录, 并且把这些内容更新到数据库中.
//    // 此方法不考虑子目录里面的内容.
//    private void scan(File path) {
//        // DEBUG 这个日志打印的有点多, 先临时去掉.
//        System.out.println("[FileManager] 扫描路径: " + path.getAbsolutePath());
//        // 1. 列出文件系统上真实的文件/目录
//        List<FileMeta> scanned = new ArrayList<>();
//        File[] files = path.listFiles();
//        if (files != null) {
//            for (File f : files) {
//                scanned.add(new FileMeta(f));
//            }
//        }
//
//        // 2. 列出数据库里当前指定目录里面的内容
//        List<FileMeta> saved = fileDao.searchByPath(path.getPath());
//
//        // 3. 找出文件系统中没有的, 数据库中有的, 把这些内容从数据库删除掉
//        List<FileMeta> forDelete = new ArrayList<>();
//        for (FileMeta fileMeta : saved) {
//            if (!scanned.contains(fileMeta)) {
//                // 这个操作就是把数据库中有的, 文件系统中没有的, 拎出来了.
//                forDelete.add(fileMeta);
//            }
//        }
//        fileDao.delete(forDelete);
//
//        // 4. 找出文件系统中有的, 数据库没有的, 把这些内容往数据库里插入.
//        List<FileMeta> forAdd = new ArrayList<>();
//        for (FileMeta fileMeta : scanned) {
//            if (!saved.contains(fileMeta)) {
//                // 找出了文件系统中有, 数据库没有的.
//                forAdd.add(fileMeta);
//            }
//        }
//        fileDao.add(forAdd);
//    }
//}